﻿using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Sustainalytics.ClientDW.Entities.Universes;
using Sustainalytics.Utils.Exceptions.Universes;
using Sustainalytics.Auth.Claims;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Sustainalytics.DataLayer;
using MongoDB.Bson.Serialization;
using Sustainalytics.Utils;

namespace Sustainalytics.ClientDW.DataAccessLayer
{
    public class UniversesRepository : IUniversesRepository
    {
        private readonly string _connectionString;
        private readonly IMongoDatabase _clientUniversesMongoDb = null;
        private readonly MongoGridFSHelper mongoGridFSHelper;

        public static IUniversesRepository GetUniversesRepository(string connectionString)
        {
            return new UniversesRepository(connectionString);
        }

        public UniversesRepository(string connectionString)
        {
            if (string.IsNullOrEmpty(connectionString))
                throw new ArgumentException("mongoDbUrl");

            var dbConnectionString = MongoUrl.Create(connectionString);

            if (string.IsNullOrWhiteSpace(dbConnectionString.DatabaseName))
            {
                throw new ArgumentException("No database specified in the connection string!");
            }

            _clientUniversesMongoDb = new MongoClient(dbConnectionString).GetDatabase(dbConnectionString.DatabaseName);

            var mongoClient = new MongoClient(connectionString);
            var database = mongoClient.GetDatabase(dbConnectionString.DatabaseName);
            mongoGridFSHelper = new MongoGridFSHelper(database);
            _connectionString = connectionString;
        }

        public List<int> GetAllUniversesIds()
        {
            return _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name)
                                            .Find(_ => true)
                                            .Project(u => u.Id)
                                            .ToList();
        }
        public List<Universe> GetAllUniversesIdsAndAccountIds()
        {
            return _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name)
                                            .Find(_ => true)
                                            .Project<Universe>(Builders<Universe>.Projection.Include(u => u.Id).Include(u => u.AccountId).Include(u => u.Name))
                                            .ToList();
        }

        public Tuple<Stream, Guid> UploadFile(Guid id, string fileName, string contentType)
        {
            return mongoGridFSHelper.OpenUploadStream(fileName, id, contentType);
        }

        public bool IsProcessingScheduled()
        {
            var coll = _clientUniversesMongoDb.GetCollection<UploadLog>(typeof(UploadLog).Name).AsQueryable();
            var log = coll.OrderByDescending(ul => ul.Created).FirstOrDefault(l => l.Created > DateTime.Today);
            if (log != null)
                return !log.Processed;
            return false;
        }

        public void AddUploadLogs(UploadLog uploadLog)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UploadLog>(typeof(UploadLog).Name);
            if (coll.AsQueryable().Any(un => un.FileId == uploadLog.FileId))
                throw new DuplicateUploadLogsException();
            coll.InsertOne(uploadLog);
        }

        public void AddFeatureUniverse(FeatureUniverse featureUniverse)
        {
            var coll = _clientUniversesMongoDb.GetCollection<FeatureUniverse>(typeof(FeatureUniverse).Name);
            if (coll.AsQueryable().Any(un => un.AccountId == featureUniverse.AccountId && un.Feature == featureUniverse.Feature))
                throw new DuplicateFeatureUniverseException();
            coll.InsertOne(featureUniverse);
        }

        public void AddUniverse(Universe universe)
        {
            var coll = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name);
            if (coll.AsQueryable().Any(un => un.Id == universe.Id || (un.AccountId == universe.AccountId && un.Name == universe.Name)))
                throw new DuplicateUniverseException();
            coll.InsertOne(universe);
        }

        public void AddIfNotExistsBatchUniverse(List<UniverseCompany> universeCompanies)
        {
            if (universeCompanies == null || universeCompanies.Count == 0)
                return;
            var coll = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name);

            foreach (var universeCompany in universeCompanies)
            {
                var queryFinfByCompanyAndUniverse = Builders<UniverseCompany>.Filter.And(
                    Builders<UniverseCompany>.Filter.Eq(universeCompany.PropertyName((t) => t.CompanyId), universeCompany.CompanyId),
                    Builders<UniverseCompany>.Filter.Eq(universeCompany.PropertyName((t) => t.UniverseId), universeCompany.UniverseId));
                var setOnInstert = Builders<UniverseCompany>.Update
                    .SetOnInsert(x => x.Id, universeCompany.Id)
                    .SetOnInsert(universeCompany.PropertyName((t) => t.CIQID), universeCompany.CIQID)
                    .SetOnInsert(universeCompany.PropertyName((t) => t.LastUpdated), universeCompany.LastUpdated)
                    .SetOnInsert(universeCompany.PropertyName((t) => t.Created), universeCompany.Created);


                coll.UpdateMany(queryFinfByCompanyAndUniverse, setOnInstert);
            }
        }

        public void DeleteAllUniverseCompanies()
        {
            var coll = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name);
            coll.DeleteMany(_ => true);
        }

        public void DeleteAllUniverses()
        {
            var coll = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name);
            coll.DeleteMany(_ => true);
        }

        public void DeleteUniverse(int universeId)
        {
            var coll = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name);
            coll.DeleteMany(x => x.Id == universeId);
        }

        public void DeleteAllFeatureUniverses()
        {
            var coll = _clientUniversesMongoDb.GetCollection<FeatureUniverse>(typeof(FeatureUniverse).Name);
            coll.DeleteMany(_ => true);
        }

        public FeatureUniverse GetFeatureUniverse(Guid accountId, AccountFeature feature)
        {
            var coll = _clientUniversesMongoDb.GetCollection<FeatureUniverse>(typeof(FeatureUniverse).Name).AsQueryable();

            return coll.SingleOrDefault(fu => fu.AccountId == accountId && fu.Feature == feature);
        }

        public byte[] GetFile(Guid fileId)
        {
            return mongoGridFSHelper.DownloadToByteArray(fileId);
        }

        public Universe GetUniverse(string universeName, Guid accountId)
        {
            var coll = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name).AsQueryable();

            return coll.SingleOrDefault(u => u.Name.ToLowerInvariant() == universeName.ToLowerInvariant() && u.AccountId == accountId);
        }

        public IEnumerable<Universe> GetUniversesIdNameByName(string universeName)
        {
            var coll = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name).AsQueryable();

            return coll.Where(u => u.Name.ToLowerInvariant() == universeName.ToLowerInvariant())
                       .Select(x => new Universe() { Id = x.Id, Name = x.Name })
                       .ToArray();
        }



        public List<Universe> GetUniversesByAccountId(Guid accountId)
        {
            var coll = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name).AsQueryable();
            return coll.Where(u => u.AccountId == accountId).ToList();

        }

        public List<Universe> GetUniversesEntitiesByNameForAccountId(Guid accountId, string univNameLike)
        {
            var coll = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name).AsQueryable();
            if (univNameLike == null)
            {
                return coll.Where(u => (u.AccountId == accountId)).ToList();
            }
            return coll.Where(u => (u.AccountId == accountId && u.Name.ToLowerInvariant().Contains(univNameLike.ToLowerInvariant()))).ToList();
        }

        public UniverseCompany GetUniverseCompany(Guid companyId, int universeId)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name).AsQueryable();

            return coll.SingleOrDefault(uc => uc.UniverseId == universeId && uc.CompanyId == companyId);
        }

        public void UpdateFeatureUniverses(Guid accountId, AccountFeature feature, List<int> universeIds)
        {
            var coll = _clientUniversesMongoDb.GetCollection<FeatureUniverse>(typeof(FeatureUniverse).Name);
            var featureUniverse = coll.AsQueryable().SingleOrDefault(fu => fu.AccountId == accountId && fu.Feature == feature);
            if (featureUniverse == null)
                throw new FeatureUniverseNullException();

            featureUniverse.UniverseIds = universeIds;

            var query = Builders<FeatureUniverse>.Filter.And(
                Builders<FeatureUniverse>.Filter.Eq(x => x.AccountId, accountId),
                Builders<FeatureUniverse>.Filter.Eq(x => x.Feature, feature));

            var update = Builders<FeatureUniverse>.Update.Set(x => x.UniverseIds, universeIds);
            coll.UpdateMany(query, update, new UpdateOptions { IsUpsert = true });
        }

        public UploadLog GetUploadLog()
        {
            var coll = _clientUniversesMongoDb.GetCollection<UploadLog>(typeof(UploadLog).Name).AsQueryable();
            return coll.OrderByDescending(c => c.Created).FirstOrDefault();
        }

        public void AddUploadErrorLog(UploadErrorLog uploadErrorLog)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UploadErrorLog>(typeof(UploadErrorLog).Name);
            coll.InsertOne(uploadErrorLog);
        }

        public List<UploadErrorLog> GetUploadErrorLogs()
        {
            var uploadColl = _clientUniversesMongoDb.GetCollection<UploadLog>(typeof(UploadLog).Name).AsQueryable();
            var coll = _clientUniversesMongoDb.GetCollection<UploadErrorLog>(typeof(UploadErrorLog).Name).AsQueryable();

            var uploadLog = uploadColl.OrderByDescending(upl => upl.Created).FirstOrDefault(d => d.Processed);

            if (uploadLog == null)
                return new List<UploadErrorLog>();


            return coll.Where(upl => upl.FileId == uploadLog.FileId).OrderByDescending(log => log.Worksheet).ThenBy(log => log.RowNumber).ToList();
        }

        public int GetNextUniverseId()
        {
            var coll = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name).AsQueryable();
            return coll.Any() ? coll.Max(c => c.Id) + 1 : 1;
        }

        public void UpdateUploadLog(UploadLog uploadLog)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UploadLog>(typeof(UploadLog).Name);
            var query = Builders<UploadLog>.Filter.Eq(x => x.Id, uploadLog.Id);
            var update = Builders<UploadLog>.Update.Set(x => x.Processed, true);
            coll.UpdateMany(query, update);
        }

        public List<UniverseCompany> GetAllCompanyUniverses(Guid companyId, DateTime lastUpdated)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name).AsQueryable();

            if (companyId == Guid.Empty)
                return coll.ToList();

            return coll.Where(cu => cu.CompanyId == companyId).ToList();
        }

        public List<FeatureUniverse> GetFeatureUniveseByAccountId(Guid accountId)
        {
            var coll = _clientUniversesMongoDb.GetCollection<FeatureUniverse>(typeof(FeatureUniverse).Name).AsQueryable();

            return coll.Where(cu => cu.AccountId == accountId).ToList();
        }


        public void DeleteUniversesByAccountId(Guid accountId)
        {
            var universeColl = _clientUniversesMongoDb.GetCollection<Universe>(typeof(Universe).Name);
            var universeCompanyColl = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name);

            var listUniverseIds = universeColl.AsQueryable().Where(uni => uni.AccountId == accountId).Select(uni => uni.Id);
            if (listUniverseIds != null && listUniverseIds.Any())
            {
                foreach (var universeId in listUniverseIds)
                {
                    universeCompanyColl.DeleteMany(x => x.UniverseId == universeId);
                }
            }

            universeColl.DeleteMany(x => x.AccountId == accountId);
        }

        public void DeleteFeatureUniversesByAccountId(Guid accountId)
        {
            var coll = _clientUniversesMongoDb.GetCollection<FeatureUniverse>(typeof(FeatureUniverse).Name);

            coll.DeleteMany(x => x.AccountId == accountId);
        }

        public List<UniverseCompany> GetUniverseCompanies(int universeId)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name);

            return coll.AsQueryable().Where(uc => uc.UniverseId == universeId).ToList();
        }

        public bool ExistsModifiedAfter(DateTime now)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name);
            return coll.AsQueryable().Where(uc => uc.Created >= now).FirstOrDefault() != null;
        }

        public List<int> GetUniverseIdsByFeatureIds(int featureId, Guid? accountId = null)
        {
            var col = _clientUniversesMongoDb.GetCollection<FeatureUniverse>(typeof(FeatureUniverse).Name);

            var query = col.AsQueryable().Where(c => c.Feature == (AccountFeature)featureId);

            if (accountId.HasValue)
            {
                query = query.Where(c => c.AccountId == accountId.Value);
            }


            return query.ToList().SelectMany(p => p.UniverseIds)
                .Distinct()
                .ToList();

        }

        public void InsertBatchUniverseCompany(List<UniverseCompany> universeCompanies)
        {
            var collection = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name);


            collection.Indexes.DropAll();
            collection.InsertMany(universeCompanies);

            collection.Indexes.CreateOne(Builders<UniverseCompany>.IndexKeys.Ascending(x => x.AccountId));
            collection.Indexes.CreateOne(Builders<UniverseCompany>.IndexKeys.Ascending(x => x.CompanyId));
            collection.Indexes.CreateOne(Builders<UniverseCompany>.IndexKeys.Ascending(x => x.UniverseId));
            collection.Indexes.CreateOne(Builders<UniverseCompany>.IndexKeys.Ascending(x => x.CIQID));
        }


        public void AddUploadErrorBatchLog(List<UploadErrorLog> uploadErrorLogs)
        {
            if (uploadErrorLogs == null || uploadErrorLogs.Count == 0)
                return;

            var coll = _clientUniversesMongoDb.GetCollection<UploadErrorLog>(typeof(UploadErrorLog).Name);
            coll.InsertMany(uploadErrorLogs);
        }

        public void DeleteUniverseCompanyByAccountId(Guid accountId)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name);

            coll.DeleteMany(x => x.AccountId == accountId);
        }

        public void DeleteUniverseCompanyByAccountIds(List<Guid> accountIds)
        {
            var coll = _clientUniversesMongoDb.GetCollection<UniverseCompany>(typeof(UniverseCompany).Name);

            coll.DeleteMany(Builders<UniverseCompany>.Filter.In(x => x.AccountId, accountIds));
        }

        public Dictionary<Guid, List<int>> GetAllUniverseCompany()
        {
            return MongoDatabaseFactory.GetDatabase(_connectionString)
                                                .GetCollection<UniverseCompany>()
                                                .Aggregate(options: new AggregateOptions() { AllowDiskUse = true })
                                                .Project(x => new { x.CompanyId, x.UniverseId })
                                                .Group(x => x.CompanyId, g => new { CompanyId = g.Key, UniverseIds = g.Select(x => x.UniverseId).ToList() })
                                                .ToList()
                                                .ToDictionary(x => x.CompanyId, x => x.UniverseIds);
        }

        public class UniverseCompanyDto
        {
            public Guid CompanyId { get; set; }
            public List<int> UniverseIds { get; set; }

            public UniverseCompanyDto(BsonDocument document)
            {
                var doc = BsonSerializer.Deserialize<UniverseCompanyDto>(document);
                this.CompanyId = doc.CompanyId;
                this.UniverseIds = doc.UniverseIds;
            }
        }
    }
}
